How we do iOS apps: Part 1 - Dependency Injection
This is the first post in a series where we describe how be build iOS apps at AppFoundry.
In this post we’ll explain Dependency Injection.
First steps
To see the advantages of Dependency Injection, we need a small example. Let’s say we are an App Development company, and we want to create iOS apps. To do this, we first of all need a designer. A designer will create designs based on a set of requirements. A developer will then take these designs and craft them into an app. Let’s look at how we might have done this in 2012.
struct SkeumorphicDesigner {
func createDesign(requirements: Requirements) -> Design {
//Draws some boxes with stitches, applies colors
//returns Design
}
}
struct ObjectiveCDeveloper {
func createApp(requirements: Requirements,
design: Design) -> App {
//Does some code between [], applies the design using
//InterfaceBuilder, returns App
}
We’ve now built a model for creating designs and developing apps. We still need to put these two guys together:
class AppFactory2012 {
private let developer:ObjectiveCDeveloper
private let designer:SkeumorphicDesigner
init() {
developer = ObjectiveCDeveloper()
designer = SkeumorphicDesigner()
}
public func createApp(requirements:Requirements) -> App {
let design = designer.createDesign(requirements)
return developer.createApp(requirements, design: design)
}
}
We can finally create an app. Here is how.
let appFactory = AppFactory2012()
let app = appFactory.createApp(requirements)
We succeeded in creating a simple model to create apps. Now let’s move on to 2013.
Code grows
In 2013, Apple released iOS 7. They decided we didn’t want skeumorphic designs anymore. Flat was the new cool kid in town. Here’s the code for our new FlatDesigner
.
struct FlatDesigner {
func createDesign(requirements: Requirements) -> Design {
//Draws some flat boxes, applies less color, returns Design
}
}
Since our AppFactory2012
is using a SkeumorphicDesigner
, we need to create another AppFactory, let’s say AppFactory2013
class AppFactory2013 {
private let developer:ObjectiveCDeveloper
private let designer:FlatDesigner
init() {
developer = ObjectiveCDeveloper()
designer = FlatDesigner()
}
public func createApp(requirements:Requirements) -> App {
let design = designer.createDesign(requirements)
return developer.createApp(requirements, design: design)
}
}
Using this new factory would look something like this:
let appFactory = AppFactory2013()
let app = appFactory.createApp(requirements)
There hardly is a difference between AppFactory2012
and AppFactory2013
. We basically copy-pasted and changed the type of the designer…
Code keeps growing!
Let’s move on to 2014 and beyond. Apple now wants us to write code in Swift, instead of ObjectiveC. In comes the SwiftDeveloper
.
struct SwiftDeveloper {
func createApp(requirements: Requirements,
design: Design) -> App {
//Does some code without semi-colons, applies the
//design using StoryBoards, returns App
}
Again, a new factory is needed, since the already existing factories have a so called tight coupling with their developers and designers.
class AppFactory2014 {
private let developer:SwiftDeveloper
private let designer:FlatDesigner
init() {
developer = SwiftDeveloper()
designer = FlatDesigner()
}
public func createApp(requirements:Requirements) -> App {
let design = designer.createDesign(requirements)
return developer.createApp(requirements, design: design)
}
}
And its usage:
let appFactory = AppFactory2014()
let app = appFactory.createApp(requirements)
Why this is bad
Needless to say, all this code copying-and-pasting leads to typical copy-paste problems: Fixing an issue in one factory, doesn’t immediately fix it in the other factories. This code will become unmaintainable pretty fast when we allow it to go on like this. So let’s take a look at where the problem lies, and what we can do about it.
Improving the situation
First of all we see that designers and developers have the same functions. That’s a good thing, we can formalize this fact using protocols. Since our developer and designer types already have these functions in place, we can just use an extension to make them adopt the specific protocols.
protocol Designer {
func createDesign(requirements: Requirements) -> Design
}
protocol Developer {
func createApp(requirements: Requirements,
design: Design) -> App
}
extension SkeumorphicDesigner : Designer {}
extension FlatDesigner : Designer {}
extension ObjectiveCDeveloper : Developer {}
extension SwiftDeveloper : Developer {}
These protocols allow us to refactor our factories. Let’s use these protocols as property types instead of their implementations. We could do this for all our different factories, for brevity we’ll show just the 2012 version.
class AppFactory2012 {
private let developer:Developer
private let designer:Designer
init() {
developer = ObjectiveCDeveloper()
designer = SkeumorphicDesigner()
}
public func createApp(requirements:Requirements) -> App {
let design = designer.createDesign(requirements)
return developer.createApp(requirements, design: design)
}
}
//This change is applied to all factories
Although a very small improvement has been made, we still have the same duplication as before. To really solve the problem, we have to get rid of the tight coupling which still exists in the initializers. They still require too much knowledge on how to create designers and developers. They don’t really care if you look at the rest of the code but we forced it on them.
Getting rid of implementation details
So the goal is to remove the tight coupling completely. To do this, we’ll have to remove all direct references to adoptions of Developer
and Designer
. One way we could do this is, instead of creating new instances, asking for them instead. A type can ask for instances of other types through its setters or through its initializer(s). We refactor the initializer like this:
//...
init(developer: Developer, designer: Designer) {
self.developer = developer
self.designer = designer
}
//...
If we apply this to all of our factories, we see that they now all have the exact same code! So we can get rid of all the duplication and just stick with one:
class AppFactory {
private let developer:Developer
private let designer:Designer
init(developer: Developer, designer: Designer) {
self.developer = developer
self.designer = designer
}
func createApp(requirements:Requirements) -> App {
let design = designer.createDesign(requirements)
return developer.createApp(requirements, design: design)
}
}
Now, to use our factory, we’ll need to pass in its dependencies:
let developer = ObjectiveCDeveloper()
let designer = SkeumorphicDesigner()
let factoryLike2012 = AppFactory(developer: developer,
designer: designer)
factoryLike2012.createApp(requirements)
And we can reuse it in other combinations:
let developer = JavaDeveloper()
let designer = MaterialDesigner()
let factoryForAndroid = AppFactory(developer: developer,
designer: designer)
factoryForAndroid.createApp(requirements)
Wait a minute! Did we just create an Android app?
Dependency Injection
To solve our code duplication problems, we applied dependency injection: we injected instances of a specific type into a loosely coupled initializer. We’ll never have an excuse again to copy-paste because of new combinations of developers and designers.
Furthermore, we are now sure that if the internals of a developer or designer change, the AppFactory
can still remain untouched.
Testing becomes easier too. Imagine creating a unit test for the AppFactory2012 class. A real unit test only tests a single unit, but this proves to be hard when it comes to our AppFactory2012
because, without us wanting to, it needs to call the ObjectiveCDeveloper
and SkeumorphicDesigner
whenever we call the createApp
function in our tests.
It becomes easier with our AppFactory
, because we can inject any developer and designer, even fake ones (mocks, stubs anyone?)
We’ll cover the subject of testing in more dept later in this series!
Conclusion
Dependency Injection allows us to write better readable, more maintainable and better testable code. It isn’t hard to do, you just need to look at your code from a different perspective. Look for tight coupled dependencies in your code and remove them by simply hiding the implementation details behind a protocol, as we did with our Designer
s and Developer
s. Then, inject those properties via setters or via an initializer.
While you’re at it, take a look at our Dependency Injection framework, called Reliant. Many more examples on how to do DI are available there.